Add static search to isolated documentation builds - #3709
Conversation
Replace PR #3709's native pagefind binary subprocess approach with the Pagefind.Net and Pagefind.Net.Frontend NuGet packages — pure managed .NET, no native binary, AOT-compatible. The pagefind indexer is now a proper IMarkdownExporter that eagerly indexes each page during ExportAsync (O(1) memory) and flushes the index to disk in FinishExportAsync. It is included in both ExportOptions.Default and ExportOptions.Validation, so it works in isolated builds and docs-builder serve mode automatically. Key changes: - New PagefindMarkdownExporter in Elastic.Markdown/Exporters/Pagefind - New Exporter.Pagefind enum value, registered in both ExporterParsers - Removed native binary pipeline (package-pagefind.mjs, embedded resource, pagefind npm dep, PagefindSearchIndexer subprocess) - Removed static-search feature flag — search is always available for isolated builds and serve mode - Serve mode reads pagefind files from the background build's in-memory filesystem via a route handler - Added .pagefind-net-frontend-version to AllowedHiddenFileNames Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Replace PR #3709's native pagefind binary subprocess approach with the Pagefind.Net and Pagefind.Net.Frontend NuGet packages — pure managed .NET, no native binary, AOT-compatible. The pagefind indexer is now a proper IMarkdownExporter that eagerly indexes each page during ExportAsync (O(1) memory) and flushes the index to disk in FinishExportAsync. It is included in both ExportOptions.Default and ExportOptions.Validation, so it works in isolated builds and docs-builder serve mode automatically. Key changes: - New PagefindMarkdownExporter in Elastic.Markdown/Exporters/Pagefind - New Exporter.Pagefind enum value, registered in both ExporterParsers - Removed native binary pipeline (package-pagefind.mjs, embedded resource, pagefind npm dep, PagefindSearchIndexer subprocess) - Removed static-search feature flag — search is always available for isolated builds and serve mode - Serve mode reads pagefind files from the background build's in-memory filesystem via a route handler - Added .pagefind-net-frontend-version to AllowedHiddenFileNames Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* Emit Mermaid diagrams as external SVG files loaded via img (#3713) Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Fix CopyBrandingResources failure during serve in-memory build (#3718) Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Fix overlapping scope roots for nested Codex config paths (#3719) docs-builder 1.29.0 added extension roots from FindGitRoot for Codex commands. When config.yml lives under environments/internal/, the resolved root is nested inside WorkingDirectoryRoot and ScopedFileSystem rejects the overlapping roots. Normalize extension roots before constructing the scoped filesystem so redundant nested paths are dropped. Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify scope root normalization for nested extension roots (#3720) Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Replace pagefind native binary with Pagefind.Net managed library Replace PR #3709's native pagefind binary subprocess approach with the Pagefind.Net and Pagefind.Net.Frontend NuGet packages — pure managed .NET, no native binary, AOT-compatible. The pagefind indexer is now a proper IMarkdownExporter that eagerly indexes each page during ExportAsync (O(1) memory) and flushes the index to disk in FinishExportAsync. It is included in both ExportOptions.Default and ExportOptions.Validation, so it works in isolated builds and docs-builder serve mode automatically. Key changes: - New PagefindMarkdownExporter in Elastic.Markdown/Exporters/Pagefind - New Exporter.Pagefind enum value, registered in both ExporterParsers - Removed native binary pipeline (package-pagefind.mjs, embedded resource, pagefind npm dep, PagefindSearchIndexer subprocess) - Removed static-search feature flag — search is always available for isolated builds and serve mode - Serve mode reads pagefind files from the background build's in-memory filesystem via a route handler - Added .pagefind-net-frontend-version to AllowedHiddenFileNames Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jan Calanog <jan.calanog@elastic.co>
Generate a Pagefind index at build time so standalone sites can provide search without requiring a backend service. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Only publish frontend resources from the static asset namespace so the Pagefind indexer remains internal to isolated builds. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Expose structured page breadcrumbs to Pagefind so isolated search results identify their documentation section without a redundant Docs prefix. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Collapse secondary labels and controls on narrow screens so standalone documentation headers remain usable without horizontal overflow. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Reduce the embedded indexer size and preserve useful page titles, breadcrumbs, and section deep links in static search results. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Keep search results focused on canonical pages and avoid changing the global heading DOM solely for section deep links. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Reuse the normalized platform target so packaging validation and executable selection cannot drift. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
* Emit Mermaid diagrams as external SVG files loaded via img (#3713) Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Fix CopyBrandingResources failure during serve in-memory build (#3718) Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Fix overlapping scope roots for nested Codex config paths (#3719) docs-builder 1.29.0 added extension roots from FindGitRoot for Codex commands. When config.yml lives under environments/internal/, the resolved root is nested inside WorkingDirectoryRoot and ScopedFileSystem rejects the overlapping roots. Normalize extension roots before constructing the scoped filesystem so redundant nested paths are dropped. Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Simplify scope root normalization for nested extension roots (#3720) Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> * Replace pagefind native binary with Pagefind.Net managed library Replace PR #3709's native pagefind binary subprocess approach with the Pagefind.Net and Pagefind.Net.Frontend NuGet packages — pure managed .NET, no native binary, AOT-compatible. The pagefind indexer is now a proper IMarkdownExporter that eagerly indexes each page during ExportAsync (O(1) memory) and flushes the index to disk in FinishExportAsync. It is included in both ExportOptions.Default and ExportOptions.Validation, so it works in isolated builds and docs-builder serve mode automatically. Key changes: - New PagefindMarkdownExporter in Elastic.Markdown/Exporters/Pagefind - New Exporter.Pagefind enum value, registered in both ExporterParsers - Removed native binary pipeline (package-pagefind.mjs, embedded resource, pagefind npm dep, PagefindSearchIndexer subprocess) - Removed static-search feature flag — search is always available for isolated builds and serve mode - Serve mode reads pagefind files from the background build's in-memory filesystem via a route handler - Added .pagefind-net-frontend-version to AllowedHiddenFileNames Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jan Calanog <jan.calanog@elastic.co>
|
@Mpdreamz Clicking search result items doesn't work. I will take a look |
|
Oh, maybe an artifact of merging in? I could swear it worked. Good catch either way! |
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Pagefind.Net 0.4.0 (nullean/pagefind-net#14) fixes multiple encoding and tokenization bugs that caused multi-word searches to fail — heading text was never indexed separately, so AND queries across title/body words returned no results. Switch from AddRecord with WeightedSegments to AddHtmlRecord with HtmlPageData. This ensures heading words enter the index with correct per-position weights matching the official Pagefind binary, and content is built by joining all sections so heading words are always searchable. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # docs/configure/content-set/index.md
0.4.1 (nullean/pagefind-net#15) fixes a bug where meta field IDs were assigned per-record alphabetically rather than globally. With both breadcrumbs and title in meta, breadcrumbs was falsely boosted as title matches and actual title content lost its boost. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Pagefind.Net 0.4.1 — meta field ordering fixUpdated to Pagefind.Net 0.4.1 which fixes nullean/pagefind-net#15. The bug: meta field IDs were assigned per-record based on alphabetical ordering of that record's meta keys. Since we store both If results are too noisy: 0.4.1 also adds an new PagefindIndexOptions { Language = "en", IndexedMetaFields = ["title"] }This would keep breadcrumbs in the fragment metadata (for display in results) but exclude it from the searchable |
CLI commands with supplemental cmd-*.md files were registered twice in DocumentationSet.Files: once from the main directory scan (under the physical cmd-*.md path) and again from the extension's ScanDocumentation Files (under the clean synthetic path). Both entries pointed to the same CliCommandFile instance, causing each page to be exported/indexed twice. Fix both halves: - Factory registers markdown files under doc.SourceFile (the canonical path) rather than the discovery path — CLI commands get their clean synthetic path directly from the main scan. - Extension's ScanDocumentationFiles skips files already in _createdFiles to avoid duplicate key exceptions. This eliminates 20 duplicate entries in the pagefind search index that were inflating scores for CLI reference pages in search results. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Summary of changes added to this PRPagefind.Net 0.4.0 → 0.4.1Upgraded to fix multiple bugs in the .NET pagefind indexer:
Switch to
|
- Cap indexed content to first 10 headings and 5 body sections per page. Reduces index chunk size by 37% (236KB → 148KB) — directly cuts network cost per search query. Pages still have title + key headings + opening content indexed, which is sufficient for discovery. - Set IndexedMetaFields=["title"] so breadcrumbs stay in fragment metadata (displayed in search results) but are excluded from searchable postings. Prevents breadcrumb text from polluting term scores. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Index size optimization & follow-upContent limits (this commit)Added caps to the pagefind indexer: max 10 headings and 5 body sections per page. Pages still have title + key headings + opening content indexed — sufficient for discovery without the long tails of CLI option tables and reference lists. Also set Impact
What loads whenPagefind adds zero bytes to initial page load — everything is lazy:
Follow-up: header load timeThe slow uncached header isn't pagefind — it's the React/EUI bundle loaded synchronously via Potential improvements (not in this PR):
|
Why
Standalone documentation sites currently have no search unless they depend on a backend service. They need a search option that continues to work when the generated frontend is hosted as static files on services such as S3 and CloudFront, or when previewing locally with
docs-builder serve.What
Adds Pagefind-powered client-side search to all isolated builds and
docs-builder servemode. The existing search modal queries the generated client-side index while omitting backend-only Ask AI features.This PR represents combined work:
How
The `PagefindMarkdownExporter` runs inline during the build pipeline alongside other exporters. For each markdown page it extracts plain text via `PlainTextExporter`, builds weighted segments (title, headings, body), and feeds a `PagefindRecord` to `PagefindIndex.AddRecord` which tokenizes and discards the content immediately. At the end, `WriteAsync` flushes the pre-built inverted index to disk and `PagefindFrontend.ExtractToAsync` writes the browser runtime (`pagefind.js` + WASM).
The exporter is gated on `BuildType.Isolated` — assembler and codex builds skip it entirely (they use the Elasticsearch-backed search API). In serve mode, the index resides in the in-memory filesystem and is served via a dedicated route handler.
Made with Cursor